Repository navigation
fix(cli): hide console windows on Windows for background/detached spawns - #3996
Conversation
Add windowsHide: true to 8 spawn/execFile call sites that were missing it: ffprobe/ffmpeg probes in the producer and engine packages, the telemetry exit-flush and studio dev-server spawns, and the browser-open and background-preview detached launches in the CLI. previewLifecycle.ts's spawnDetachedPreview redirects stdout/stderr to a raw log-file descriptor; libuv only adds Windows' CREATE_NO_WINDOW flag when no stdio slot inherits a raw handle, so windowsHide alone does not fully suppress a console flash there (documented inline) — it's still added for consistency and because it correctly sets the process's show-window state regardless. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
…erver spawn tests - The previewLifecycle.ts comment (and its mirrored test comment) overstated the residual console-flash risk on the detached background preview spawn: DETACHED_PROCESS already leaves that child without a console regardless of whether CREATE_NO_WINDOW gets applied. - Add the two missing regression tests for preview.ts's runDevMode and runLocalStudioMode spawns, exporting both for test access. - Drop an unused export in audioPadTrim.ts flagged by the repo's dead-code gate; the constant is only ever used within its own file. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
… comparison psnr.ts's execFile call runs in-process inside the Studio server's render path (parallelCoordinator.ts's drawElement disk self-verify), so a render started from Studio's UI under a detached preview hits this call from a console-less parent — the same flash case the other windowsHide sites in this PR already cover. psnrFilterAvailability.ts's probe is on the same in-process render path and already had the flag. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
…er windows visible windowsHide maps to libuv UV_PROCESS_WINDOWS_HIDE, which also starts GUI apps with SW_HIDE, so the detached browser launch must not set it. The background preview child already has no console (DETACHED_PROCESS with inherited log fds), so its flag was a no-op. The telemetry, psnr and audioPadTrim sites shipped on main via #3839 and #4604.
feac689 to
3104833
Compare
Edit accuracy: accurate 1216 (base branch 1216), smooth 1075 of thoseThe gate passes. Quarantined, measured but not gated (1)
|
With windowsHide the dev server runs in its own hidden console on Windows, so closing the terminal only signals the wrapper (SIGHUP). Handle it like SIGINT and SIGTERM and kill the process tree.
The background preview has no console, so its ffmpeg loudness run opened a visible window on Windows.
somanshreddy
left a comment
There was a problem hiding this comment.
First pass at 22156fb5. This is a comment, not an approval. Two independent passes went into it, mine and Codex's (static, in a separate checkout), and I checked every point below at this head. CI is green (94 passed).
What holds up
- The four new flags are correct for their stdio. All four spawns use pipes or
ignore, so libuv addsCREATE_NO_WINDOWand the child gets a hidden console of its own. - The two dropped sites are right to drop.
previewLifecycle.tspasses a log fd, so libuv would skipCREATE_NO_WINDOWanyway, andDETACHED_PROCESSalready leaves the child with no console.openBrowserlaunches a GUI app.
- Everything else I traced from
previewis either hidden already or never runs in the console-less background server. I walked the import graph fromcommands/preview.ts(449 modules) and checked every spawn in it:- studio-server's
waveform,peakMap(×2),proxyTranscoder(×2),mediaMetadataandfreezeFramealready setwindowsHide; - engine, producer, parsers and lint have no unhidden spawns;
- the Windows branches in
orphanCleanupandprocessTree(PowerShell, taskkill) are hidden; portUtilsnetstat is only reached from--list/--stopin the foreground (Codex reached the same conclusion);- the telemetry
pmset/vm_statcalls are darwin-only; apt-getinbrowser/manageris Linux-only;- Studio's registry install passes
skipClipboard: true, soclip.exenever runs.
- studio-server's
- The tests catch removal of each change. I removed each
windowsHidein turn (dev mode, local studio,transcodeToMp4,measureAudio), and each removal fails its test. DroppingSIGHUPfrom the signal list fails 3 tests. - Test runs: the touched CLI files pass 41 of 41, and studio-server's
loudnesstests pass 25 of 25.
Should fix
-
Studio's background removal still opens a visible console the first time it's used (
utils/optionalPackages.ts:142). The description listsoptionalPackages.tsnpm as foreground-only, but this path runs inside the background server:- Studio's media route calls
startBackgroundRemoval(studio-server/src/routes/media.ts:238); - that runs
background-removal/pipeline(server/studioServer.ts:668); - which calls
createSession→loadOptionalPackage("onnxruntime-node", "background removal")(inference.ts:71); - on first use that runs
install→runNpm.
On Windows
runNpmspawnscmd.exe /d /s /c npm.cmd install onnxruntime-node@… --prefix …(npxCommand.ts:14) with piped stdio and nowindowsHide. In the background server, which has no console, that cmd.exe gets a new visible console window for the whole install.Background is the default whenever the shell isn't interactive (
preview.ts:632), so agent-driven previews take this path. Embedded mode is the normal installed-CLI path, and there the route runs in that console-less process.windowsHide: trueon that spawn is safe for the foreground callers too (snapshot,transcribeand the embedder), because their output is piped either way. Codex found this too and rated it blocking. I'd call it should-fix because it only happens once per machine and the feature still works. - Studio's media route calls
-
Nothing tests that the shutdown handler actually kills the dev server, and on Windows this PR makes it the only thing that does (
preview.ts:1340). I replacedkillProcessTree(child.pid)inshutdownwith a no-op, and all 40preview.test.tstests still pass.- The new "reaps the dev server when the terminal closes (SIGHUP)" test uses an already-exited child with no
pid. It checks that the listener is registered and removed, but never calls it. - Before this PR, a foreground dev server shared the user's console. Ctrl+C and closing the terminal reached it directly.
- With
CREATE_NO_WINDOWit sits in its own hidden console, as the description says. So on Windows, Ctrl+C and terminal close now stop it only through this listener'staskkill /T /F.
A test with a pending child that has a
pidwould cover it. It would call the captured SIGHUP (or SIGINT) listener, assert that the tree kill ran (mock../utils/orphanCleanup.js), and then emitexit. Codex found the SIGHUP half of this. - The new "reaps the dev server when the terminal closes (SIGHUP)" test uses an already-exited child with no
Where I disagreed with Codex
- Codex also flagged
studio/vite.producer.ts:26(bun run --filter @hyperframes/producer buildwithstdio: "pipe"and nowindowsHide) as a background dev-mode console flash. I don't think it can flash after this PR. That build runs inside the Vite host, which now has a hidden console of its own. A child withoutwindowsHideattaches to its parent's console, and Windows only creates a new console when the parent has none. Adding the flag there for consistency is harmless, but nothing currently opens a window from it.
Nit
runDevMode,runLocalStudioModeandtranscodeToMp4are exported only so the tests can import them. That's fine, but a one-line comment on each would stop someone from treating them as public API.
— Somu
jrusso1020
left a comment
There was a problem hiding this comment.
Reviewed at 22156fb5, the full diff plus the surrounding code in preview.ts and orphanCleanup.ts. This adds to the earlier review on this head, which I agree with. Approving.
What I checked:
- The four spawns (
preview.tsrunDevModeandrunLocalStudioMode,init.tstranscodeToMp4,loudness.tsmeasureAudio) all use pipes orignore. SowindowsHidedoes what the description says: libuv addsCREATE_NO_WINDOW, and nothing redirects to an fd that would make it skip that. - The SIGHUP change (
preview.ts:1342,:1359): oneSTUDIO_CHILD_SHUTDOWN_SIGNALSlist drives bothonceandoff, so a signal can't be registered and then left behind. That leak is what the comment at :1357 warns about. killProcessTree(orphanCleanup.ts:28) is fire-and-forget, which suits a console-close handler. Windows gives the process a few seconds afterCTRL_CLOSE_EVENT, and the tree kill only has to be started within that time, not finished.- Dropping
openBrowserandpreviewLifecycleis right, for the reasons in the description.
The earlier review's two points, which I'd take as follow-ups rather than blockers:
utils/optionalPackages.ts:142still spawns npm with nowindowsHide. The background server reaches it through background removal's first-use install, so the description's "foreground-only" is wrong for that one site. This isn't a regression from this PR, and it's a one-line fix.- Nothing calls the shutdown listener on a live child with a
pid. On Windows that listener is now the only thing that stops the dev server on Ctrl+C or terminal close, so it deserves a test where the tree kill is mocked and asserted.
Verdict: APPROVE
Reasoning: A small, correct change, with a test for each flag that fails when the flag is removed. The remaining gaps are one pre-existing spawn and one missing test, not regressions.
— Rames Jusso
Summary
Adds
windowsHide: trueto the spawns on the preview and background-server path that origin/main still leaves without it:preview.tsrunDevMode: thebun run devstudio dev server.preview.tsrunLocalStudioMode: the local Vite studio server.studio-serverloudness.tsmeasureAudio: the ffmpeg loudness measurement behind Studio's "Normalize loudness", which runs inside the background preview server.init.tstranscodeToMp4: the ffmpeg transcode, matching the other ffmpeg spawns on main.A background preview runs without a console (
DETACHED_PROCESS). Any console child it spawns withoutwindowsHidegets a new, visible console window on Windows for as long as that child runs.With the flag, the studio dev server sits in its own hidden console. Closing the terminal now signals only the CLI wrapper, with SIGHUP.
waitForStudioChildClosehandled only SIGINT and SIGTERM, so the wrapper would die and leave the dev server running, still holding the port. It now handles SIGHUP the same way, killing the process tree, as render cancellation already does.The remaining CLI spawns without the flag are foreground-only and share the user's console, so they cannot open a window (for example
portUtils.tsnetstat,optionalPackages.tsnpm,skills.tsnpx). This follows the existing pattern: main sets the flag at each call site (#3839, #4604). There is no shared spawn wrapper these sites could route through.runCancellableProcessbuffers output and waits for exit, so it cannot run a long-lived streaming dev server.Dropped from the original change
flushSync,psnr.ts,psnrFilterAvailability.tsandaudioPadTrim.tsalready have the flag on main (fix: unbreak the regression suite on Windows (PSNR filtergraph escaping + console window popups) #3839, fix(cli): commands exit right away even when the network or DNS is broken #4604).openBrowser.ts: Node mapswindowsHideto libuvUV_PROCESS_WINDOWS_HIDE. That flag also setsSTARTF_USESHOWWINDOWwithwShowWindow = SW_HIDE(libuvsrc/win/process.c), so a browser started this way can open hidden. A browser is a GUI app and never gets a console, so the flag only adds that risk.previewLifecycle.ts: the background preview child redirects its stdio to a log fd. libuv therefore skipsCREATE_NO_WINDOW, andDETACHED_PROCESSalready leaves the child without a console, so the flag would do nothing.Test plan
preview.test.ts:runDevModeandrunLocalStudioModepasswindowsHide: truetospawn, andwaitForStudioChildCloseregisters and removes a SIGHUP handler.init.transcodeToMp4.windowsHide.test.tsandloudness.windowsHide.test.ts: the ffmpeg spawns passwindowsHide: true.